Limpieza de datos#

Librerias#

# Librerias
import warnings
warnings.filterwarnings('ignore')

import pandas as pd
import os
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

from fuzzywuzzy import process
# Definir la paleta de colores personalizada
custom_colors = ['#1F3040', '#B9CDCA', '#F2C6AC', '#D99982', '#735749']
color_histograma = custom_colors[3]
color_linea = custom_colors[4]

Cargar datos#

## Ruta de la carpeta que contiene los archivos CSV

#carpeta = 'C:/UNINORTE/VC/Proyecto2/dataset_ventas'

##Obtener la lista de archivos CSV en la carpeta

#archivos_csv = [archivo for archivo in os.listdir(carpeta) if archivo.endswith('.csv')]

## Crear un DataFrame vacío para almacenar los datos combinados

#ventas = pd.DataFrame()

## Leer cada archivo CSV y combinarlo en el DataFrame datos_combinados

#for archivo in archivos_csv:
#    ruta_archivo = os.path.join(carpeta, archivo)
#    datos_archivo = pd.read_csv(ruta_archivo, index_col=0)
#    ventas = pd.concat([ventas, datos_archivo], ignore_index=True)

## Descargar los datos en un excel

#ventas.to_excel('datos.xlsx', index=False)

Después de fusionar todos los documentos CSV, se procede a descargar el archivo final. A partir de este momento, utilizaremos este archivo para continuar con el trabajo.

ventas = pd.read_excel('C:/UNINORTE/VC/Proyecto2/datos.xlsx')
ventas.head(5)
lat long id date category location mode price details description surface rooms baths park
0 NaN -0.000105 6416237 2021-06-20 Apartamento Bogotá Galerias Venta $148.000.000 ['Área Const.:\r 44,00 ... ApartamentoInteriorPrimerPisoremodeladoSalaCom... _x000D_44,00m²_x000D__x000D_ _x000D__x000D_Habitaciones:_x000D_3_x000D__x000D_ _x000D__x000D_Baños:_x000D_1_x000D__x000D_ _x000D__x000D_Sinespecificar_x000D__x000D_
1 NaN -0.000105 6416237 2021-06-20 Apartamento Bogotá Galerias Venta $148.000.000 ['Área Const.:\r 44,00 ... ApartamentoInteriorPrimerPisoremodeladoSalaCom... _x000D_44,00m²_x000D__x000D_ _x000D__x000D_Habitaciones:_x000D_3_x000D__x000D_ _x000D__x000D_Baños:_x000D_1_x000D__x000D_ _x000D__x000D_Sinespecificar_x000D__x000D_
2 NaN -0.000105 6416237 2021-06-20 Apartamento Bogotá Galerias Venta $148.000.000 ['Área Const.:\r 44,00 ... ApartamentoInteriorPrimerPisoremodeladoSalaCom... _x000D_44,00m²_x000D__x000D_ _x000D__x000D_Habitaciones:_x000D_3_x000D__x000D_ _x000D__x000D_Baños:_x000D_1_x000D__x000D_ _x000D__x000D_Sinespecificar_x000D__x000D_
3 NaN -0.000105 6416237 2021-06-20 Apartamento Bogotá Galerias Venta $148.000.000 ['Área Const.:\r 44,00 ... ApartamentoInteriorPrimerPisoremodeladoSalaCom... _x000D_44,00m²_x000D__x000D_ _x000D__x000D_Habitaciones:_x000D_3_x000D__x000D_ _x000D__x000D_Baños:_x000D_1_x000D__x000D_ _x000D__x000D_Sinespecificar_x000D__x000D_
4 NaN NaN 6461572 2021-07-25 Apartamento Pereira El nogal Venta $215.000.000 ['Área Const.:\r 65,00 ... FantásticoApartamentoubicadoenelclubresidencia... _x000D_65,00m²_x000D__x000D_ _x000D__x000D_Habitaciones:_x000D_3_x000D__x000D_ _x000D__x000D_Baños:_x000D_2_x000D__x000D_ _x000D__x000D_Parqueaderos:1_x000D__x000D_

Limpieza de datos#

# Reemplazar "Aconsultar" por NaN en la columna price
ventas['price'] = ventas['price'].replace('Aconsultar', np.nan)
# Eliminar el signo $, los puntos y los espacios en blanco de la columna price
ventas['price'] = ventas['price'].str.replace('[\$,\. ]', '', regex=True)

# Convertir la columna price a tipo numérico
ventas['price'] = pd.to_numeric(ventas['price'])
# Convertir la columna date a formato de fecha
ventas['date'] = pd.to_datetime(ventas['date'])
# Establecer valores fuera del rango válido como NaN en la columna 'lat', es decir, todos lo valores que esten fuera de las latitudes mínima y máxima de Colombia
ventas.loc[(ventas['lat'] < -4.227) | (ventas['lat'] > 12.450), 'lat'] = np.nan

# Establecer valores fuera del rango válido como NaN en la columna 'long', es decir, todos lo valores que esten fuera de las longitudes mínima y máxima de Colombia
ventas.loc[(ventas['long'] < -79.000) | (ventas['long'] > -67.000), 'long'] = np.nan
# Dividir la columna 'location' en dos nuevas columnas: 'ciudad' y 'barrio'
ventas[['ciudad', 'barrio']] = ventas['location'].str.split(' ', n=1, expand=True)

# Eliminar la columna 'location'
ventas.drop(columns=['location'], inplace=True)
# Eliminar "_x000D_" de las columnas especificadas
cols_to_clean = ['surface', 'rooms', 'baths', 'park']
for col in cols_to_clean:
    ventas[col] = ventas[col].str.replace('_x000D_', '', regex=False)
# Eliminar "m²", puntos y comas de la columna 'surface', convertir a formato numérico y dividir por 100 para que se tenga en cuenta los decimales
ventas['surface'] = ventas['surface'].str.replace('m²', '', regex=False).str.replace('.', '', regex=False).str.replace(',', '', regex=False).astype(float) / 100
# Reemplazar "Sinespecificar" por NaN y eliminar "Habitaciones:" de la columna 'rooms'
ventas['rooms'] = ventas['rooms'].replace('Sinespecificar', np.nan).str.replace('Habitaciones:', '', regex=False).astype(float)

# Reemplazar "Sinespecificar" por NaN y eliminar "Baños:" de la columna 'baths'
ventas['baths'] = ventas['baths'].replace('Sinespecificar', np.nan).str.replace('Baños:', '', regex=False).astype(float)
# Limpiar y transformar la columna 'park'
ventas['park'] = ventas['park'].replace('Sinespecificar', '0')\
                               .str.replace('Parqueaderos:', '', regex=False)\
                               .replace('Másde10', '11')\
                               .astype(float)
# Eliminar filas donde 'mode' es "Arriendo"
ventas = ventas[ventas['mode'] != 'Arriendo']

# Eliminar la columna 'mode'
ventas.drop(columns=['mode'], inplace=True)
# Palabras a eliminar
words_to_remove = ['VENTA', 'VENDO', 'APTO', 'BARRIO']

# Eliminar palabras específicas y repetición del nombre de la ciudad
ventas['barrio'] = ventas.apply(lambda row: ' '.join(word for word in (row['barrio'] or '').split() if word.upper() not in words_to_remove and word.upper() != row['ciudad'].upper()), axis=1)

A continuación, trataremos por separado la columna details, dado que tiene muchos datos útiles para el análisis, pero están juntos dentro de una misma columna, de forma que no se pueden interpretar correctamente.

# Crear un nuevo DataFrame solo con la columna 'details'
nuevo_df = pd.DataFrame(ventas['details'])

# Dividir la columna 'details' en varias columnas separando el texto por '''
nuevo_df = nuevo_df['details'].str.split("'", expand=True)

# Renombrar las columnas del nuevo DataFrame
nuevo_df.columns = [f'detalle_{i}' for i in range(nuevo_df.shape[1])]
# Lista de columnas a eliminar
columnas_a_eliminar = ['detalle_0', 'detalle_2', 'detalle_4', 'detalle_6', 'detalle_8', 'detalle_10', 'detalle_12', 'detalle_14', 'detalle_16', 'detalle_18', 'detalle_20']

# Eliminar las columnas especificadas
nuevo_df.drop(columns=columnas_a_eliminar, inplace=True)
# Nombres de las columnas del nuevo DataFrame
nombres_columnas = ["Área privada", "Área Const.", "Precio m²", "Admón", "Estrato", "Estado", "Antigüedad", "Piso No", "Tipo de Apartamento", "Sector"]

# Crear el nuevo DataFrame con las columnas especificadas
df_final = pd.DataFrame(columns=nombres_columnas)

# Función para extraer el valor asociado a un nombre de columna en una fila del DataFrame original
def extraer_valor(fila, nombre_columna):
    for elemento in fila:
        if isinstance(elemento, str) and nombre_columna.lower() in elemento.lower():
            return elemento.split(':')[1].strip()
    return None

# Llenar el nuevo DataFrame buscando los valores correspondientes en cada fila de nuevo_df
for nombre_columna in nombres_columnas:
    df_final[nombre_columna] = nuevo_df.apply(lambda fila: extraer_valor(fila, nombre_columna), axis=1)
df_final.head()
Área privada Área Const. Precio m² Admón Estrato Estado Antigüedad Piso No Tipo de Apartamento Sector
0 None \r 44,00 m² \r 3.363.636/m² None \r 4\r \r None \r 9 a 15 años None None Ver Mapa
1 None \r 44,00 m² \r 3.363.636/m² None \r 4\r \r None \r 9 a 15 años None None Ver Mapa
2 None \r 44,00 m² \r 3.363.636/m² None \r 4\r \r None \r 9 a 15 años None None Ver Mapa
3 None \r 44,00 m² \r 3.363.636/m² None \r 4\r \r None \r 9 a 15 años None None Ver Mapa
4 None \r 65,00 m² \r 3.307.692/m² None \r 4\r \r None None None None Ver Mapa

A continuación, concatenar esta nueva información con nuestro dataframe ‘ventas’

# Concatenar los DataFrames a lo largo del eje de las columnas
ventas = pd.concat([ventas, df_final], axis=1)

Eliminaremos las siguientes columnas:

  • ‘details’: La información contenida en esta columna ya está divida en otras columnas (pasos realizados anteriormente).

  • ‘description’: Es un valor único para cada vivienda y es subjetivo.

  • ‘date’: Es la fecha en la que se cargó la información de la vivienda a la pagina, lo cual no influye en el costo de la vivienda.

  • ‘id’: Es el código asignado a la vivienda al momento del registro y es un valor único, no influye en el costo de la vivienda.

  • ‘Precio m²’: Es una columna calculada a partir de la columna ‘price’ y ‘Área const.’.

# Eliminar la columna 'details'
ventas.drop(columns=['details'], inplace=True)

# Eliminar la columna 'description'
ventas.drop(columns=['description'], inplace=True)

# Eliminar la columna 'date'
ventas.drop(columns=['date'], inplace=True)

# Eliminar la columna 'Precio m²'
ventas.drop(columns=['Precio m²'], inplace=True)
# Limpieza y conversión de las columnas "Área privada" y "Área Const." en el DataFrame ventas
for columna in ["Área privada", "Área Const."]:
    ventas[columna] = (ventas[columna]
                       .str.replace('m²', '', regex=False)
                       .str.replace('.', '', regex=False)
                       .str.replace(',', '', regex=False)
                       .str.replace('\r', '', regex=False)
                       .str.replace('\\r', '', regex=False)
                       .str.strip()
                       .astype(float) / 100)
# Limpieza de la columna "Admón"
ventas['Admón'] = (ventas['Admón']
                   .str.replace('$', '', regex=False)
                   .str.replace(',', '', regex=False)
                   .str.replace('\r', '', regex=False)
                   .str.replace('\\r', '', regex=False)
                   .str.replace('Incluida', '0', regex=False)
                   .str.strip()
                   .astype(float))
# Limpieza de la columna "Estrato"
ventas['Estrato'] = (ventas['Estrato']
                     .str.strip()
                     .str.replace('\r', '', regex=False)
                     .str.replace('\\r', '', regex=False)
                     .replace('Campestre', np.nan)
                     .replace('                    Campestre                    ', np.nan)
                     .astype(float))
# Limpieza de la columna "Estado"
ventas['Estado'] = (ventas['Estado']
                    .str.replace('\r', '', regex=False)
                    .str.replace('\\r', '', regex=False)
                    .str.strip())
# Mostrar los valores únicos en la columna "Estado"
valores_unicos_estado = ventas['Estado'].unique()
print(valores_unicos_estado)
[None 'Bueno' 'Excelente' 'Remodelar']

A continuación, cambiaremos los valores de la columna estado para pasarlos a números, teniendo en cuenta que esto es una calificación de la vivienda.

# Cambiar los valores en la columna "Estado"
ventas['Estado'] = ventas['Estado'].replace({'Remodelar': 3, 'Bueno': 4, 'Excelente': 5})
# Cambiar el nombre de la columna "Antigüedad" a "Antiguedad"
ventas.rename(columns={'Antigüedad': 'Antiguedad'}, inplace=True)
# Limpieza de la columna "Antigüedad"
ventas['Antiguedad'] = (ventas['Antiguedad']
                        .str.replace('\\r', '', regex=False)    
                        .str.strip())
# Limpieza de la columna "Piso No"
ventas['Piso No'] = (ventas['Piso No']
                     .str.replace('\r', '', regex=False)
                     .str.replace('\\r', '', regex=False)   
                     .str.replace('º', '', regex=False)
                     .str.replace('ª', '', regex=False)
                     .str.strip()
                     .replace('Otros', np.nan)
                     .astype(float))
# Limpieza de la columna "Tipo de Apartamento"
ventas['Tipo de Apartamento'] = (ventas['Tipo de Apartamento']
                                 .str.replace('\r', '', regex=False)
                                 .str.replace('\\r', '', regex=False)  
                                 .str.strip())
# Cambiar valores en 'Tipo de Apartamento' a 'No Aplica' cuando 'category' es 'Casa'
ventas.loc[ventas['category'] == 'Casa', 'Tipo de Apartamento'] = 'No Aplica'
# Limpieza de la columna "Sector"
ventas['Sector'] = (ventas['Sector']
                    .str.replace('Ver Mapa', '', regex=False)
                    .replace('', np.nan)  # Reemplazar cadenas vacías resultantes con NaN
                    .str.strip())
# Restablecer el índice del DataFrame
ventas = ventas.reset_index(drop=True)

Eliminar columnas duplicadas:

  • Los duplicados se eliminarán de acuerdo con los id de vivienda duplicados y teniendo en cuenta que las filas que queden sean las más completas.

# Eliminar filas duplicadas manteniendo la fila con más datos completos - Para aseguridad de que un mismo inmueble no tuviera 2 id diferentes
ventas = (ventas.sort_values(by=ventas.columns.tolist(), na_position='last')
                .drop_duplicates(subset=['id'], keep='first')
                .drop_duplicates())

Eliminar columna:

  • ‘id’: Es el código asignado a la vivienda al momento del registro y es un valor único, no influye en el costo de la vivienda.

# Eliminar la columna 'id'
ventas.drop(columns=['id'], inplace=True)
ventas.head(10)
lat long category price surface rooms baths park ciudad barrio Área privada Área Const. Admón Estrato Estado Antiguedad Piso No Tipo de Apartamento Sector
407717 -3.556624 NaN Apartamento 197000000.0 64.0 2.0 2.0 1.0 Cali Ciudad Bochalema 64.0 64.0 178000.0 4.0 NaN 1 a 8 años NaN None Ciudad Bochalema
407715 -2.889093 NaN Casa 650000000.0 192.0 5.0 3.0 0.0 Bogotá Antiguo Copihue 192.0 192.0 NaN 3.0 NaN 1 a 8 años NaN No Aplica Zona Norte
407713 -2.577413 -73.038536 Apartaestudio 70000000.0 30.0 1.0 1.0 0.0 Bogotá Las Lomas 30.0 30.0 45000.0 2.0 NaN 16 a 30 años NaN None Las Lomas
407710 -2.193166 NaN Apartamento 235000000.0 56.0 3.0 2.0 1.0 Medellín Calasanz Occidente 56.0 56.0 192139.0 3.0 NaN None 13.0 None NaN
407708 -2.037440 NaN Apartaestudio 139000000.0 43.0 1.0 1.0 1.0 Barranquilla Ciudad Jardín 43.0 43.0 145000.0 4.0 NaN None 4.0 None NaN
407706 -1.054628 -73.300781 Apartamento 260000000.0 115.0 4.0 2.0 0.0 Medellín Centro NaN 115.0 NaN 4.0 4.0 9 a 15 años 3.0 None Centro
407703 -0.341669 -78.530228 Apartamento 175000000.0 63.0 3.0 2.0 1.0 Cartagena Ciudad Jardin 63.0 63.0 115000.0 3.0 NaN None 7.0 None NaN
407700 -0.181263 NaN Apartamento 153000000.0 74.0 3.0 2.0 1.0 Barranquilla Miramar 74.0 74.0 190000.0 4.0 NaN None 5.0 None NaN
407699 -0.175781 -77.255859 Casa 390000000.0 160.0 6.0 3.0 1.0 Medellín Occidente 160.0 160.0 NaN 3.0 4.0 16 a 30 años 1.0 No Aplica Occidente
407696 -0.148553 NaN Apartamento 145000000.0 54.0 3.0 1.0 0.0 Bogotá Ciudad Tintal 54.0 54.0 NaN 3.0 NaN 9 a 15 años NaN None Ciudad Tintal
ventas = ventas.reset_index(drop=True)
ventas['Estrato'] = ventas['Estrato'].astype('category')
ventas['Estado'] = ventas['Estado'].astype('category')
ventas['category'] = ventas['category'].astype('category')
ventas.head()
lat long category price surface rooms baths park ciudad barrio Área privada Área Const. Admón Estrato Estado Antiguedad Piso No Tipo de Apartamento Sector
0 -3.556624 NaN Apartamento 197000000.0 64.0 2.0 2.0 1.0 Cali Ciudad Bochalema 64.0 64.0 178000.0 4.0 NaN 1 a 8 años NaN None Ciudad Bochalema
1 -2.889093 NaN Casa 650000000.0 192.0 5.0 3.0 0.0 Bogotá Antiguo Copihue 192.0 192.0 NaN 3.0 NaN 1 a 8 años NaN No Aplica Zona Norte
2 -2.577413 -73.038536 Apartaestudio 70000000.0 30.0 1.0 1.0 0.0 Bogotá Las Lomas 30.0 30.0 45000.0 2.0 NaN 16 a 30 años NaN None Las Lomas
3 -2.193166 NaN Apartamento 235000000.0 56.0 3.0 2.0 1.0 Medellín Calasanz Occidente 56.0 56.0 192139.0 3.0 NaN None 13.0 None NaN
4 -2.037440 NaN Apartaestudio 139000000.0 43.0 1.0 1.0 1.0 Barranquilla Ciudad Jardín 43.0 43.0 145000.0 4.0 NaN None 4.0 None NaN
ventas.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 169528 entries, 0 to 169527
Data columns (total 19 columns):
 #   Column               Non-Null Count   Dtype   
---  ------               --------------   -----   
 0   lat                  169191 non-null  float64 
 1   long                 149730 non-null  float64 
 2   category             169528 non-null  category
 3   price                169515 non-null  float64 
 4   surface              169526 non-null  float64 
 5   rooms                167914 non-null  float64 
 6   baths                168167 non-null  float64 
 7   park                 169528 non-null  float64 
 8   ciudad               169528 non-null  object  
 9   barrio               169528 non-null  object  
 10  Área privada         120204 non-null  float64 
 11  Área Const.          169526 non-null  float64 
 12  Admón                105780 non-null  float64 
 13  Estrato              167101 non-null  category
 14  Estado               98550 non-null   category
 15  Antiguedad           139813 non-null  object  
 16  Piso No              101698 non-null  float64 
 17  Tipo de Apartamento  57552 non-null   object  
 18  Sector               134150 non-null  object  
dtypes: category(3), float64(11), object(5)
memory usage: 21.2+ MB

Análisis Descriptivo#

ventas.describe().T
count mean std min 25% 50% 75% max
lat 169191.0 5.037213e+00 2.789628e+00 -3.556624e+00 4.594397e+00 4.704000e+00 6.189920e+00 1.140061e+01
long 149730.0 -7.485683e+01 8.842583e-01 -7.853023e+01 -7.556703e+01 -7.419706e+01 -7.406600e+01 -6.713100e+01
price 169515.0 1.923391e+10 5.534035e+12 1.530000e+02 2.500000e+08 3.950000e+08 6.800000e+08 1.989188e+15
surface 169526.0 2.467323e+03 3.904603e+05 1.000000e+00 6.800000e+01 1.000000e+02 1.720000e+02 1.000000e+08
rooms 167914.0 3.278226e+00 2.059622e+00 1.000000e+00 3.000000e+00 3.000000e+00 4.000000e+00 2.540000e+02
baths 168167.0 2.719404e+00 1.523848e+00 1.000000e+00 2.000000e+00 2.000000e+00 3.000000e+00 2.530000e+02
park 169528.0 1.293946e+00 1.237870e+00 0.000000e+00 1.000000e+00 1.000000e+00 2.000000e+00 1.100000e+01
Área privada 120204.0 1.948304e+03 5.486953e+05 1.000000e+00 6.700000e+01 1.000000e+02 1.720000e+02 1.900000e+08
Área Const. 169526.0 2.467323e+03 3.904603e+05 1.000000e+00 6.800000e+01 1.000000e+02 1.720000e+02 1.000000e+08
Admón 105780.0 1.824785e+06 3.205592e+07 -1.794967e+09 1.750000e+05 3.150000e+05 5.600000e+05 1.900000e+09
Piso No 101698.0 4.193730e+00 3.111030e+00 1.000000e+00 2.000000e+00 3.000000e+00 5.000000e+00 1.600000e+01

Conclusiónes parciales de la información anteriór:

  • Price: Hay una enorme variabilidad con valores desde 152 hasta 1.989188e+15. La desviación estándar es también extremadamente alta, indicando la presencia de valores atípicos significativos que afectan la media y la dispersión.

  • Surface y Área Const: Ambas variables tienen el mismo resumen estadístico, lo que podría indicar duplicidad o un error en el reporte. El valor máximo de 100.000.000 y el mínimo es 1, son valores inusuales para superficies habitacionales, lo que indica la presencia de valores atípicos.

  • baths: El valor máximo de 253 para baños es improbable para propiedades residenciales y sugiere un error.

  • Admón: Se evidencian datos atípicos y errores, dado que tiene valores negativos y un máximo extremadamente alto, además de una gran desviación estándar.

Análisis de datos atípicos#

ventas = pd.read_csv('C:/UNINORTE/VC/Proyecto2/Archivos_Finales/datos2.csv')
ventas.describe().T
count mean std min 25% 50% 75% max
lat 169191.0 5.037213e+00 2.789628e+00 -3.556624e+00 4.594397e+00 4.704000e+00 6.189920e+00 1.140061e+01
long 149730.0 -7.485683e+01 8.842583e-01 -7.853023e+01 -7.556703e+01 -7.419706e+01 -7.406600e+01 -6.713100e+01
price 169515.0 1.923391e+10 5.534035e+12 1.530000e+02 2.500000e+08 3.950000e+08 6.800000e+08 1.989188e+15
surface 169526.0 2.467323e+03 3.904603e+05 1.000000e+00 6.800000e+01 1.000000e+02 1.720000e+02 1.000000e+08
rooms 167914.0 3.278226e+00 2.059622e+00 1.000000e+00 3.000000e+00 3.000000e+00 4.000000e+00 2.540000e+02
baths 168167.0 2.719404e+00 1.523848e+00 1.000000e+00 2.000000e+00 2.000000e+00 3.000000e+00 2.530000e+02
park 169528.0 1.293946e+00 1.237870e+00 0.000000e+00 1.000000e+00 1.000000e+00 2.000000e+00 1.100000e+01
Área privada 120204.0 1.948304e+03 5.486953e+05 1.000000e+00 6.700000e+01 1.000000e+02 1.720000e+02 1.900000e+08
Área Const. 169526.0 2.467323e+03 3.904603e+05 1.000000e+00 6.800000e+01 1.000000e+02 1.720000e+02 1.000000e+08
Admón 105780.0 1.824785e+06 3.205592e+07 -1.794967e+09 1.750000e+05 3.150000e+05 5.600000e+05 1.900000e+09
Estrato 167101.0 4.354959e+00 1.280686e+00 1.000000e+00 3.000000e+00 4.000000e+00 6.000000e+00 6.000000e+00
Estado 98550.0 4.492532e+00 5.439049e-01 3.000000e+00 4.000000e+00 5.000000e+00 5.000000e+00 5.000000e+00
Piso No 101698.0 4.193730e+00 3.111030e+00 1.000000e+00 2.000000e+00 3.000000e+00 5.000000e+00 1.600000e+01

Dado que la columna ‘Admón’ tiene valores negativos y esto en la práctica no tiene sentido, estos valores serán reemplazados por nulos.

# Replace negative values with NaN in the 'Admón' column
ventas['Admón'] = ventas['Admón'].apply(lambda x: np.nan if x < 0 else x)

Contar valores ‘cero’ en cada columna.

# Count zero values in each column
zero_counts = (ventas == 0).sum()

# Print the counts of zero values
print(zero_counts)
lat                    19342
long                       0
category                   0
price                      0
surface                    0
rooms                      0
baths                      0
park                   40868
ciudad                     0
barrio                     0
Área privada               0
Área Const.                0
Admón                   1147
Estrato                    0
Estado                     0
Antiguedad                 0
Piso No                    0
Tipo de Apartamento        0
Sector                     0
dtype: int64
# Replace cero values with NaN in the 'lat' column
ventas['lat'] = ventas['lat'].replace(0, np.nan)

Funciones#

Función para detección de valores atípicos mediante el rango intercuantil (IQR)

def detect_outliers(df, column, lower_percentile, upper_percentile):
    # Calcular el primer y el cuartil superior basado en los percentiles proporcionados
    Q1 = df[column].quantile(lower_percentile)
    Q3 = df[column].quantile(upper_percentile)
    
    # Calcular el rango intercuartílico (IQR)
    IQR = Q3 - Q1
    
    # Definir los límites para los valores atípicos
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    
    # Identificar los valores atípicos
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)][column]
    
    # Mostrar un análisis descriptivo de los valores atípicos
    print("Análisis descriptivo de valores atípicos en '{}':".format(column))
    print(outliers.describe().to_frame().T)
    
    # Calcular y mostrar el porcentaje de valores atípicos
    porcentaje = (len(outliers) / len(df)) * 100
    print("\nPorcentaje de valores atípicos: {:.2f}%".format(porcentaje))
    
    cant=len(outliers)
    print("\nCantidad de valores atípicos: {}".format(cant))
    
    # Retornar los valores atípicos y sus límites para uso adicional si es necesario
    return outliers, lower_bound, upper_bound, cant

Función para detección de valores atípicos mediante las puntuaciones \(Z\)

def detect_outliers_zscore(data, column):
    # Extraer la columna deseada del DataFrame
    datacol = data[column].dropna()  # Asegúrate de eliminar NaNs para el cálculo
    thres = 3  # Umbral para la puntuación Z

    # Calcular la media y la desviación estándar
    mean = np.mean(datacol)
    std = np.std(datacol)
    
    # Detectar outliers
    outliers = []
    for i in datacol:
        z_score = (i - mean) / std
        if np.abs(z_score) > thres:
            outliers.append(i)
    
    # Convertir la lista de outliers en un DataFrame para análisis
    outliers_df = pd.DataFrame(outliers, columns=['Outliers'])

    # Realizar un análisis descriptivo de los outliers detectados
    print("Análisis descriptivo de los valores atípicos en '{}':".format(column))
    if not outliers_df.empty:
        print(outliers_df.describe().T)

    # Calcular y mostrar el porcentaje de outliers
    porcentaje_outliers = (len(outliers) / len(datacol)) * 100
    print("\nPorcentaje de valores atípicos en '{}': {:.2f}%".format(column, porcentaje_outliers))

    # Opcional: devolver el DataFrame de outliers y otros datos para uso futuro
    return outliers_df, mean, std, porcentaje_outliers

Función para prueba Rosner

def rosner_test(data, k):
    # Convertir los datos a un array de NumPy y asegurarse de manejar los índices correctamente
    data = np.array(data)
    indices = np.arange(len(data))  # Crear un array de índices
    mean = np.mean(data)
    std = np.std(data)
    outliers_pr = []
    outlier_indices = []

    for _ in range(k):
        z_scores = np.abs((data - mean) / std)
        max_z = np.max(z_scores)
        if max_z > 3:  # Umbral típico de Z-score para outliers
            max_index = np.argmax(z_scores)
            outliers_pr.append(data[max_index])
            outlier_indices.append(indices[max_index])  # Guardar el índice del outlier
            data = np.delete(data, max_index)  # Eliminar el outlier para la siguiente iteración
            indices = np.delete(indices, max_index)  # Eliminar el índice del outlier
            mean = np.mean(data)  # Recalcular la media
            std = np.std(data)  # Recalcular la desviación estándar
        else:
            break

    return outliers_pr, outlier_indices

Surface y Area Const#

# Gráfico de dispersión 
fig = px.scatter(ventas, x='surface', y='Área Const.', title='Comparación de Surface y Área Const.',
                 labels={'surface': 'Surface (m²)', 'Área Const.': 'Área Const. (m²)'})
fig.update_traces(marker=dict(color=custom_colors[0]))  
fig.update_layout(
    xaxis_title='Surface (m²)',
    yaxis_title='Área Const. (m²)'
)
fig.show()
# Crear el gráfico de caja
fig = px.box(ventas, y=['surface', 'Área Const.'], title='Box Plot de Surface y Área Const.',
             labels={'variable': 'Variable', 'value': 'Superficie (m²)'},
             color='variable', 
             color_discrete_sequence=custom_colors)  
# Mostrar el gráfico
fig.show()
ventas_suryare = ventas.dropna(subset=['surface', 'Área Const.'])  # Elimina filas donde cualquiera de las dos columnas es NaN

# Calcular la correlación de Pearson entre las dos columnas
correlation = ventas_suryare['surface'].corr(ventas_suryare['Área Const.'])

# Imprimir el coeficiente de correlación
print("El coeficiente de correlación de Pearson entre 'surface' y 'Área Const.' es:", correlation)
El coeficiente de correlación de Pearson entre 'surface' y 'Área Const.' es: 0.9999999999999999

Con una correlación de 0.99 y como es evidente en el gráfico de dispersión y en el grafico de caja, estas 2 columnas son iguales, por ende, eliminaremos una de ellas, en este caso ‘surface’.

# Eliminar la columna 'Surface'
ventas = ventas.drop(columns=['surface'])

# Renombrar las columnas 'Área Const.' y 'Área privada'
ventas = ventas.rename(columns={
    'Área Const.': 'area_const',
    'Área privada': 'area_privada'
})
# Colores personalizados
color_histograma = custom_colors[3]
color_linea = custom_colors[4]
color_box = custom_colors[0]

# Crear una figura con subplots, especificando el espacio entre subplots
fig = make_subplots(rows=1, cols=2,
                    subplot_titles=('Histograma de área construida', 'Box Plot de Área Construida'),
                    column_widths=[0.5, 0.5],  # Ajustar según sea necesario
                    horizontal_spacing=0.2)  # Espacio entre las columnas

# Agregar histograma al primer subplot
fig.add_trace(
    go.Histogram(
        x=ventas['area_const'],
        opacity=0.8,
        marker=dict(color=color_histograma, line=dict(color=color_linea, width=1.5)),
        name='Área Construida'
    ),
    row=1, col=1
)

# Agregar box plot al segundo subplot
fig.add_trace(
    go.Box(
        y=ventas['area_const'],
        marker_color=color_box,
        name='Área Construida'
    ),
    row=1, col=2
)

# Actualizar los títulos de los ejes y el layout general
fig.update_layout(
    title_text='Distribución de Área Construida',
    showlegend=False
)
fig.update_xaxes(title_text='Área Construida (m²)', row=1, col=1)
fig.update_yaxes(title_text='Frecuencia', row=1, col=1)
fig.update_yaxes(title_text='Área Construida (m²)', row=1, col=2)

# Mostrar la figura
fig.show()

Inicialmente colocaremos como nulos todos los valores de área construida que sean superiores a \(2.500 m²\), teniendo en cuenta que los datos que tenemos corresponden a casas, apartamentos y apartaestudios y no es posible que alguno de estos inmuebles tenga un área construida superior a ese valor. Estos corresponden al \(0.31%\) de nuestros datos.

Adicionalmente, según las siguientes noticias de medios de comunicación como Noticias Caracol y El Tiempo, las casas más grandes de Colombia no superan los \(2.500 m²\) ni el valor de \(18\) mil millones de pesos colombianos:

# Contar los valores mayores que 2500
valores_mayores_2500 = ventas[ventas['area_const'] > 2500]['area_const'].count()

# Calcular el total de entradas no nulas en la columna 'area_const'
total_entradas = ventas['area_const'].notnull().sum()

# Calcular el porcentaje
porcentaje_mayores_2500 = (valores_mayores_2500 / total_entradas) * 100

# Imprimir los resultados
print("Cantidad de valores mayores que 2500 en 'area_const':", valores_mayores_2500)
print("Porcentaje de valores mayores que 2500 en 'area_const': {:.2f}%".format(porcentaje_mayores_2500))
Cantidad de valores mayores que 2500 en 'area_const': 524
Porcentaje de valores mayores que 2500 en 'area_const': 0.31%
# Marcar valores superiores a 2500 en 'area_const' como NaN
ventas.loc[ventas['area_const'] > 2500, 'area_const'] = np.nan

Continuamos con el análisis de datos atipicos para la columna ‘area_const’.

  • Detección de valores atípicos mediante las puntuaciones \(Z\)

outliers_df, mean, std, porc_outliers = detect_outliers_zscore(ventas, 'area_const')
Análisis descriptivo de los valores atípicos en 'area_const':
           count        mean         std    min    25%    50%    75%     max
Outliers  2764.0  842.622012  349.465854  559.0  612.0  723.0  900.0  2500.0

Porcentaje de valores atípicos en 'area_const': 1.64%
  • Detección de valores atípicos mediante el rango intercuantil (IQR)

outliers, lower_bound, upper_bound, cant = detect_outliers(ventas, 'area_const', 0.25, 0.75)
Análisis descriptivo de valores atípicos en 'area_const':
              count        mean         std    min    25%    50%    75%  \
area_const  12246.0  506.665418  251.797554  326.0  365.0  424.0  540.0   

               max  
area_const  2500.0  

Porcentaje de valores atípicos: 7.22%

Cantidad de valores atípicos: 12246
  • Prueba Rosner

outliers_pr, outlier_indices = rosner_test(ventas['area_const'].dropna(), k=cant)
# Imprimir comparación de cantidad de outliers
print("Después de la prueba Rosner, la cantidad de datos atípicos es:", len(outliers_pr))
print("Comparado con la cantidad de datos atípicos detectados por el método IQR que es:", cant)
Después de la prueba Rosner, la cantidad de datos atípicos es: 12246
Comparado con la cantidad de datos atípicos detectados por el método IQR que es: 12246
# Marcar como nulos los outliers detectados por la prueba Rosner en el DataFrame original
for idx in outliers.index:
    if idx in ventas.index:  # Verificar si el índice está en el DataFrame
        ventas.at[idx, 'area_const'] = np.nan

# Verificar el cambio en el DataFrame
print("\nActualización completada en el DataFrame. Total de NaN ahora en 'area_const':", ventas['area_const'].isna().sum())
Actualización completada en el DataFrame. Total de NaN ahora en 'area_const': 12772
# Crear un histograma del área construida
fig = px.histogram(ventas, x='area_const', title='Histograma de área construida',
                   labels={'area_const': 'Área Construida'},
                   opacity=0.8, color_discrete_sequence=[color_histograma])

# Personalizar el gráfico
fig.update_layout(xaxis_title='Área Construida (m²)', yaxis_title='Frecuencia')
fig.update_traces(marker_line_width=1.5, marker_line_color=color_linea)

# Mostrar el gráfico
fig.show()
# Definir el color deseado para el box plot
color_box = '#1F3040'

# Crear el box plot de área construida
fig = px.box(ventas, y=['area_const'], title='Box Plot de Área Construida',
             labels={'variable': 'Variable', 'value': 'Área Construida (m²)'},
             color_discrete_sequence=[color_box])

# Personalizar el gráfico
fig.update_layout(xaxis_title='Variable', yaxis_title='Área Construida (m²)')

# Mostrar el gráfico
fig.show()

Área Privada#

# Colores personalizados
color_histograma = custom_colors[3]
color_linea = custom_colors[4]
color_box = custom_colors[0]

# Crear una figura con subplots
fig = make_subplots(rows=1, cols=2,
                    subplot_titles=('Distribución de Área Privada', 'Boxplot de Área Privada'),
                    column_widths=[0.5, 0.5],  # Ajustar según sea necesario
                    horizontal_spacing=0.2)  # Espacio entre las columnas

# Agregar histograma al primer subplot
fig.add_trace(
    go.Histogram(
        x=ventas['area_privada'],
        opacity=0.8,
        marker=dict(color=color_histograma, line=dict(color=color_linea, width=1.5)),
        name='Área Privada'
    ),
    row=1, col=1
)

# Agregar box plot al segundo subplot
fig.add_trace(
    go.Box(
        y=ventas['area_privada'],
        marker_color=color_box,
        name='Área Privada'
    ),
    row=1, col=2
)

# Actualizar los títulos de los ejes y el layout general
fig.update_layout(
    title_text='Análisis de Área Privada',
    showlegend=False
)
fig.update_xaxes(title_text='Área Privada (m²)', row=1, col=1)
fig.update_yaxes(title_text='Frecuencia', row=1, col=1)
fig.update_yaxes(title_text='Área Privada (m²)', row=1, col=2)

# Mostrar la figura
fig.show()

Basados en la información de las noticias compartidas en el análisis de la variable área construida, eliminaremos todos los valores superiores a \(2.500 m^2\).

# Contar los valores mayores que 2500
valores_mayores_2500 = ventas[ventas['area_privada'] > 2500]['area_privada'].count()

# Calcular el total de entradas no nulas en la columna 'area_privada'
total_entradas = ventas['area_privada'].notnull().sum()

# Calcular el porcentaje
porcentaje_mayores_2500 = (valores_mayores_2500 / total_entradas) * 100

# Imprimir los resultados
print("Cantidad de valores mayores que 2500 en 'area_privada':", valores_mayores_2500)
print("Porcentaje de valores mayores que 2500 en 'area_privada': {:.2f}%".format(porcentaje_mayores_2500))
Cantidad de valores mayores que 2500 en 'area_privada': 509
Porcentaje de valores mayores que 2500 en 'area_privada': 0.42%
# Marcar valores superiores a 2500 en 'area_privada' como NaN
ventas.loc[ventas['area_privada'] > 2500, 'area_privada'] = np.nan
  • Detección de valores atípicos mediante las puntuaciones \(Z\)

outliers_df, mean, std, porc_outliers = detect_outliers_zscore(ventas, 'area_privada')
Análisis descriptivo de los valores atípicos en 'area_privada':
           count         mean         std    min    25%    50%     75%     max
Outliers  1819.0  1066.801539  424.441119  628.0  742.0  902.0  1300.0  2500.0

Porcentaje de valores atípicos en 'area_privada': 1.52%
  • Detección de valores atípicos mediante el rango intercuantil (IQR)

outliers, lower_bound, upper_bound, cant = detect_outliers(ventas, 'area_privada', 0.25, 0.75)
Análisis descriptivo de valores atípicos en 'area_privada':
               count       mean        std    min    25%    50%    75%     max
area_privada  8647.0  559.02965  334.01563  325.0  369.0  437.0  592.5  2500.0

Porcentaje de valores atípicos: 5.10%

Cantidad de valores atípicos: 8647
  • Prueba Rosner

outliers_pr, outlier_indices = rosner_test(ventas['area_privada'].dropna(), k=cant)
# Imprimir comparación de cantidad de outliers
print("Después de la prueba Rosner, la cantidad de datos atípicos es:", len(outliers_pr))
print("Comparado con la cantidad de datos atípicos detectados por el método IQR que es:", cant)
Después de la prueba Rosner, la cantidad de datos atípicos es: 8647
Comparado con la cantidad de datos atípicos detectados por el método IQR que es: 8647
# Marcar como nulos los outliers detectados por la prueba Rosner en el DataFrame original
for idx in outliers.index:
    if idx in ventas.index:  # Verificar si el índice está en el DataFrame
        ventas.at[idx, 'area_privada'] = np.nan

# Verificar el cambio en el DataFrame
print("\nActualización completada en el DataFrame. Total de NaN ahora en 'area_privada':", ventas['area_privada'].isna().sum())
Actualización completada en el DataFrame. Total de NaN ahora en 'area_privada': 58480
# Marcar como NaN los valores menores a 2 en 'area_privada'
ventas.loc[ventas['area_privada'] < 2, 'area_privada'] = np.nan

# Comparar 'area_privada' con 'area_const' y ajustar según la condición
ventas.loc[ventas['area_privada'] > ventas['area_const'], 'area_privada'] = ventas['area_const']
# Histograma de Área Privada
fig_histogram = px.histogram(ventas, x='area_privada', title='Distribución de Área Privada',
                             color_discrete_sequence=[color_histograma])
fig_histogram.update_layout(xaxis_title='Área Privada (m²)', yaxis_title='Frecuencia')
fig_histogram.update_traces(marker_line_width=1, marker_line_color=color_linea)
fig_histogram.show()

# Boxplot de Área Privada
fig_boxplot = px.box(ventas, y='area_privada', title='Boxplot de Área Privada',
                     color_discrete_sequence=[color_box])
fig_boxplot.update_layout(yaxis_title='Área Privada (m²)')
fig_boxplot.show()
#ventas.to_csv('datos3.csv', index=False)